Passed
Push — main ( 5cacf5...da0f78 )
by Pedro
02:28
created

Format.supportedLocales   F

Complexity

Conditions 32

Size

Total Lines 11
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 32

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 11
ccs 1
cts 1
cp 1
rs 0
c 0
b 0
f 0
cc 32
crap 32

How to fix   Complexity   

Complexity

Complex classes like Format.supportedLocales often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/*
2
 * decimal.js-i18n v0.2.6
3
 * Full internationalization support for decimal.js.
4
 * MIT License
5
 * Copyright (c) 2022 Pedro José Batista <[email protected]>
6
 * https://github.com/pjbatista/decimal.js-i18n
7
 */
8 1
import Decimal from "decimal.js";
9
import type BaseFormatOptions from "./baseOptions";
10
import type FormatCompactDisplay from "./compactDisplay";
11 1
import { BIGINT_MODIFIERS, ECMA_LIMIT, LOCALES, PLAIN_MODIFIERS } from "./constants";
12
import type FormatCurrency from "./currency";
13
import type FormatCurrencyDisplay from "./currencyDisplay";
14
import type FormatCurrencySign from "./currencySign";
15
import type FormatLocale from "./locale";
16
import type FormatLocaleMatcher from "./localeMatcher";
17
import type FormatNotation from "./notation";
18
import type FormatNumberingSystem from "./numberingSystem";
19
import type FormatOptions from "./options";
20 1
import { extend, resolve, toEcma, validate } from "./options";
21
import type FormatPart from "./part";
22 1
import { exponents, fractions, integerGroups, integers, PartValue } from "./part";
23
import type FormatPartTypes from "./partTypes";
24
import type ResolvedFormatOptions from "./resolvedFormatOptions";
25
import type FormatSignDisplay from "./signDisplay";
26
import type FormatStyle from "./style";
27
import type FormatTrailingZeroDisplay from "./trailingZeroDisplay";
28
import type FormatUnit from "./unit";
29
import type FormatUnitDisplay from "./unitDisplay";
30
import type FormatUseGrouping from "./useGrouping";
31
32
// Calculates an exponential value using base₁₀
33 1
const defaultLocales = LOCALES.slice();
34
35 1
const concatenate = <T extends PartValue>(filter: T[] | ((p: T) => boolean), parts: T[] = []) => {
36 82582
    if (typeof filter === "function") {
37 82568
        parts = parts.filter(filter);
38
    } else {
39 14
        parts = filter;
40
    }
41
42 82582
    return parts.map(p => p.value).join("");
43
};
44
45 5520
const pow10 = (exponent: Decimal.Value) => Decimal.pow(10, exponent);
46
47
/**
48
 * The `Decimal.Format` object enables language-sensitive decimal number formatting. It is entirely based on
49
 * `Intl.NumberFormat`, with the options of the latter being 100% compatible with it.
50
 *
51
 * This class, however, extend the numeric digits constraints of `Intl.NumberFormat` from 21 to 1000000000 in
52
 * order to fully take advantage of the arbitrary-precision of `decimal.js`.
53
 *
54
 * @template TNotation Numeric notation of formatting.
55
 * @template TStyle Numeric style of formatting.
56
 */
57 1
export class Format<TNotation extends FormatNotation = "standard", TStyle extends FormatStyle = "decimal"> {
58 1
    static readonly [Symbol.toPrimitive] = Format;
59 1169
    readonly [Symbol.toStringTag] = "Decimal.Format";
60
61
    /**
62
     * Formats a number according to the locale and formatting options of this {@link Format} object.
63
     *
64
     * @param value A valid [Decimal.Value](https://mikemcl.github.io/decimal.js/#decimal) to format.
65
     * @returns Formatted localized string.
66
     */
67
    readonly format: (value: Decimal.Value) => string;
68
69
    /**
70
     * Allows locale-aware formatting of strings produced by `Decimal.Format` formatters.
71
     *
72
     * @param value A valid [Decimal.Value](https://mikemcl.github.io/decimal.js/#decimal) to format.
73
     * @returns An array of objects containing the formatted number in parts.
74
     */
75
    readonly formatToParts: (value: Decimal.Value) => FormatPart[];
76
77
    /**
78
     * Returns a new object with properties reflecting the locale and number formatting options computed during
79
     * initialization of this {@link Decimal.Format} object.
80
     *
81
     * @returns A new object with properties reflecting the locale and number formatting options computed
82
     *   during the initialization of this object.
83
     */
84
    readonly resolvedOptions: () => ResolvedFormatOptions<TNotation, TStyle>;
85
86
    /**
87
     * Creates a new instance of the `Decimal.Format` object.
88
     *
89
     * @param locales A string with a [BCP 47](https://www.rfc-editor.org/info/bcp47) language tag, or an array
90
     *   of such strings.
91
     *
92
     *   For the general form and interpretation of this parameter, see the [Intl page on
93
     *   MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl).
94
     * @param options Object used to configure the behavior of the string localization.
95
     * @throws `RangeError` when an invalid option is given.
96
     */
97
    constructor(locales?: FormatLocale | FormatLocale[], options?: FormatOptions<TNotation, TStyle>) {
98 1169
        options ??= {};
99
100
        // 1. Check if options do not extrapolate the limits of decimal.js
101 1169
        const valid = validate(options);
102
103 1169
        if (valid !== true) {
104
            // -> it will either be exactly true or contain an array with all faulty properties:
105 5
            throw new RangeError(`${valid.join()} value${valid.length === 1 ? " is" : "s are"} out of range."`);
106
        }
107
108
        // 2. Create a baseline native formatter native
109 1164
        const ecmaOptions = toEcma(options);
110 1164
        const ecmaFormat = new Intl.NumberFormat(locales, ecmaOptions);
111
112
        // 3. Resolve this object's options, using the native resolution as a baseline
113 1164
        const resolved = resolve(options, ecmaFormat.resolvedOptions());
114 1164
        const { minimumIntegerDigits: minID, notation, rounding, style } = resolved;
115
116
        // 4. Create two auxiliary formatters:
117
        // One for the integer part, which can have up to a billion minimum digits...
118 1164
        const bigintOptions = extend(ecmaOptions, BIGINT_MODIFIERS);
119 1164
        const bigintFormat = new Intl.NumberFormat(locales, bigintOptions);
120
121
        // ...and another for a plain, localized reference, used for decimals and constants
122 1164
        const plainOptions = extend(bigintOptions, PLAIN_MODIFIERS);
123 1164
        const plainFormat = new Intl.NumberFormat(locales, plainOptions);
124
125
        // 5. Localized numeric constants
126 1164
        const numbers = Array(10)
127
            .fill(null)
128 11640
            .map((_, index) => plainFormat.format(index));
129 1164
        const numberMatch = new RegExp("[" + numbers.join("") + "]", "g");
130 1164
        const minusSign = /−/gu;
131
132
        // 5.1. Localized zero and one used in substitutions
133 1164
        const [zero, one] = numbers;
134
135
        // 5.2. Helper functions
136 4590
        const indexOfValue = (value: string) => numbers.indexOf(value).toString();
137 4584
        const convert = (text: string) => text.replaceAll(numberMatch, indexOfValue).replaceAll(minusSign, "-");
138 1164
        const zeroFill = (size: number) => Array(size).fill(zero).join("");
139 1164
        const zeroTrim = (text: string) => {
140 20642
            let result = text;
141
142 20642
            while (result[0] === zero && result.length > 1) {
143 335
                result = result.substring(1);
144
            }
145
146 20642
            return result;
147
        };
148
149
        // #region Step 6. Main format method - - - - - - - - - - - - - - - - - - - - - - - - - - - -
150 1164
        const _formatToParts = (value: Decimal.Value) => {
151 25226
            value = new Decimal(value);
152 25226
            const sign = value.s;
153
154
            // 6.1. Create a baseline part array
155 25226
            const ecmaParts = ecmaFormat.formatToParts(value.toNumber());
156
157
            // -> if the value is non-numeric or an infinity, the baseline is good enough
158 25226
            if ((value.isFinite && !value.isFinite()) || (value.isNaN && value.isNaN())) {
159 4584
                return ecmaParts;
160
            }
161
162
            // 6.2. Splitting the parts for easier assembly
163 20642
            const ecmaExponentValue = concatenate(exponents, ecmaParts) || "0";
164 20642
            const ecmaIntegerParts = ecmaParts.filter(integerGroups);
165 20642
            const ecmaIntegerTrimmed = zeroTrim(concatenate(integers, ecmaIntegerParts));
166 20642
            const ecmaIntegerDigits = concatenate(integers, ecmaIntegerParts).length;
167 20642
            const ecmaIntegerTrimmedDigits = ecmaIntegerTrimmed.length;
168 20642
            const ecmaFractionValue = concatenate(fractions, ecmaParts);
169 20642
            const ecmaFractionDigits = ecmaFractionValue.length;
170
171
            // 6.3. Shifting exponents according to notation/style
172
173
            // 6.3.1. Compact notation: calculate the shift in integer digits, and therefore exponent
174 20642
            if (notation === "compact" && !value.eq(0)) {
175 3664
                const baseInteger = value.abs().trunc().toFixed();
176 3664
                const baseIntegerDigits = baseInteger.length;
177 3664
                const correctionDigits = baseIntegerDigits - ecmaIntegerTrimmedDigits;
178
179 3664
                if (correctionDigits > 0) {
180 888
                    value = value.mul(pow10(-correctionDigits));
181
                }
182
            }
183
184
            // 6.3.2. Engr./Scientific notations: evaluate the exponent from the text
185 20642
            if ((notation === "engineering" || notation === "scientific") && ecmaExponentValue !== zero) {
186 4584
                const exponential = new Decimal(convert(ecmaExponentValue));
187 4584
                value = value.mul(pow10(exponential.mul(-1))).abs().mul(sign); // prettier-ignore
188
            }
189
190
            // 6.3.3. Percent style: shift the value accordingly (non numeric parts will remain the same)
191 20642
            if (style === "percent") value = value.mul(100);
192
193
            // 6.4. Parsing the information about the numeric parts
194 20642
            const integer = value.abs().trunc().mul(sign);
195 20642
            const fraction = value.sub(integer).abs();
196 20642
            const integerDigits = !value.eq(0) && integer.eq(0) ? 0 : value.abs().trunc().toFixed().length;
197 20642
            const fractionDigits = value.dp();
198 20642
            const maxSD = resolved.maximumSignificantDigits ?? resolved.maximumFractionDigits! + integerDigits;
199 20642
            const maxFD = resolved.maximumFractionDigits ?? maxSD - integerDigits;
200 20642
            const minSD = resolved.minimumSignificantDigits ?? resolved.minimumFractionDigits! + integerDigits;
201 20642
            const minFD = resolved.minimumFractionDigits ?? minSD - integerDigits;
202
203
            // 6.5. Check for the possibility of the native formatter to have accomplished the desired output
204 20642
            const integerCheck = !ecmaIntegerParts.length || (minID <= ECMA_LIMIT && ecmaIntegerDigits >= minID);
205 20642
            const fractionCheck = !ecmaFractionDigits || (minFD < ECMA_LIMIT && ecmaFractionDigits >= minFD);
206
207
            // -> if the native formatter is good enough for our decimal value, leave it as-is
208 20642
            if (integerCheck && fractionCheck) {
209 20612
                return ecmaParts as FormatPart[];
210
            }
211
212
            // 6.6. Create the integer value
213 30
            const integerParts = (() => {
214 30
                if (integerCheck) return ecmaIntegerParts;
215
216
                // Expanding the integer part
217 19
                const targetDigits = Math.max(integerDigits, minID);
218
219
                // Creates a base 10 power of the target digits
220 19
                const bigint = BigInt(pow10(targetDigits - 1).toFixed());
221
222
                // Format using the bigint formatter and cut it before joining with the ECMA parts
223 19
                const bigintIntegerParts = bigintFormat.formatToParts(bigint).filter(integerGroups);
224
225
                // We need to replace the first 'one' (from the base 10 power) with a 'zero'
226 19
                bigintIntegerParts[0].value = bigintIntegerParts[0].value.replace(new RegExp(one), zero);
227
228
                // Merge the first part with the bigint part
229 19
                ecmaIntegerParts[0].value =
230
                    bigintIntegerParts[ecmaIntegerParts.length - 1].value.slice(0, -ecmaIntegerParts[0].value.length) +
231
                    ecmaIntegerParts[0].value;
232
233 19
                return [...bigintIntegerParts.slice(0, -ecmaIntegerParts.length), ...ecmaIntegerParts];
234
            })();
235
236
            // 6.7. Create the fraction value
237 30
            const fractionValue = (() => {
238 30
                if (fractionCheck) return ecmaFractionValue;
239
240
                // Simpler formatting if there is actually no fraction
241 29
                if (fraction.eq(0)) {
242 13
                    return plainFormat.format(BigInt(pow10(minFD).toFixed())).slice(1);
243
                }
244
245 16
                let suffix = "";
246
247 16
                const targetDigits = maxFD - 1;
248
                // Exponential value of the fraction (converting from decimal to bigint)
249 16
                const exponential = fraction.toDP(targetDigits, rounding).mul(pow10(targetDigits)).toFixed();
250
251
                // First, create a zero-filled right-side expansion if the digits are insufficient
252 16
                if (fractionDigits < minFD) {
253 15
                    suffix = zeroFill(minFD - targetDigits);
254
                }
255
256 16
                const fractionValue = plainFormat.format(BigInt(exponential)) + suffix;
257
258
                // If the value is still not enough, it needs more left-zero-filling
259 16
                if (fractionValue.length < minFD) {
260 3
                    return zeroFill(minFD - fractionValue.length) + fractionValue;
261
                }
262
263 13
                return fractionValue;
264
            })();
265
266
            // 6.8. Parsing the numeric fragments in a unified part array
267 30
            const result: FormatPart[] = [];
268 30
            let integerDone = false;
269 30
            let fractionDone = false;
270
271 30
            while (ecmaParts.length) {
272 383
                const { type, value } = ecmaParts.shift()!;
273
274 383
                if (type === "integer" || type === "group") {
275 280
                    if (!integerDone) {
276 30
                        integerDone = true;
277 30
                        result.push(...integerParts);
278
                    }
279 280
                    continue;
280
                }
281
282 103
                if (type === "fraction") {
283 29
                    if (!fractionDone) {
284 29
                        fractionDone = true;
285 29
                        result.push({ type, value: fractionValue });
286
                    }
287 29
                    continue;
288
                }
289
290 74
                result.push({ type, value });
291
            }
292 30
            return result;
293
        };
294
        //#endregion
295
296 1164
        this.format = value => concatenate(_formatToParts(value));
297 25212
        this.formatToParts = value => _formatToParts(value);
298 1164
        this.resolvedOptions = () => ({ ...resolved });
299
    }
300
301
    /**
302
     * Returns an array containing the default locales available to the environment, based on a default
303
     * dictionary of locales and regions.
304
     *
305
     * **Note:** This method is non-standard and not available on `Intl` formatters.
306
     *
307
     * @returns Array of strings with the available locales.
308
     */
309
    static supportedLocales(): FormatLocale[] {
310 2
        return Intl.NumberFormat.supportedLocalesOf(defaultLocales);
311
    }
312
313
    /**
314
     * Returns an array containing those of the provided locales that are supported without having to fall back
315
     * to the runtime's default locale.
316
     *
317
     * @template TNotation Numeric notation of formatting.
318
     * @template TStyle Numeric style of formatting.
319
     * @param locales A string with a BCP 47 language tag, or an array of such strings. For the general form
320
     *   and interpretation of the locales argument, see the [Intl page on
321
     *   MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl).
322
     * @param options Object used to configure the behavior of the string localization.
323
     * @returns Array of strings with the available locales.
324
     */
325
    static supportedLocalesOf<TNotation extends FormatNotation = "standard", TStyle extends FormatStyle = "decimal">(
326
        locales: string | string[],
327
        options?: FormatOptions<TNotation, TStyle>,
328
    ) {
329 2
        return Intl.NumberFormat.supportedLocalesOf(locales, options ? toEcma(options) : undefined) as FormatLocale[];
330
    }
331
}
332
// eslint-disable-next-line @typescript-eslint/no-namespace
333
export declare namespace Format {
334
    export type {
335
        BaseFormatOptions,
336
        FormatCompactDisplay,
337
        FormatCurrency,
338
        FormatCurrencyDisplay,
339
        FormatCurrencySign,
340
        FormatLocale,
341
        FormatLocaleMatcher,
342
        FormatNotation,
343
        FormatNumberingSystem,
344
        FormatOptions,
345
        FormatPart,
346
        FormatPartTypes,
347
        ResolvedFormatOptions,
348
        FormatSignDisplay,
349
        FormatStyle,
350
        FormatTrailingZeroDisplay,
351
        FormatUnit,
352
        FormatUnitDisplay,
353
        FormatUseGrouping,
354
    };
355
}
356
export type {
357
    BaseFormatOptions,
358
    FormatCompactDisplay,
359
    FormatCurrency,
360
    FormatCurrencyDisplay,
361
    FormatCurrencySign,
362
    FormatLocale,
363
    FormatLocaleMatcher,
364
    FormatNotation,
365
    FormatNumberingSystem,
366
    FormatOptions,
367
    FormatPart,
368
    FormatPartTypes,
369
    ResolvedFormatOptions,
370
    FormatSignDisplay,
371
    FormatStyle,
372
    FormatTrailingZeroDisplay,
373
    FormatUnit,
374
    FormatUnitDisplay,
375
    FormatUseGrouping,
376
};
377
378
export default Format;
379